package org.ihelpuoo.xml;

import java.io.IOException;
import java.io.Writer;

import org.ihelpuoo.I;
import org.ihelpuoo.lang.StringUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;

public class XMLWriter extends Writer
{
	public static XMLWriter wrap(Writer w)
	{
		if (w instanceof XMLWriter)
			return (XMLWriter)w;
		return new XMLWriter(w);
	}

	public static final String	INDENT_TAB				= "\t";
	public static final String	INDENT_2WHITESPACE		= "  ";
	public static final String	INDENT_4WHITESPACE		= "    ";

	private static final int	DEFAULT_REF_LINE_WIDTH	= 80;

	private Writer				w;
	private boolean				isAutoFormat;
	private String				indentBy;
	private int					referredLineWidth;

	private XMLWriter(Writer w)
	{
		this.w = w;
		isAutoFormat = false;
		indentBy = INDENT_TAB;
		referredLineWidth = DEFAULT_REF_LINE_WIDTH;
	}

	public boolean isAutoFormat()
	{
		return isAutoFormat;
	}

	public void setAutoFormat(boolean isAutoFormat)
	{
		this.isAutoFormat = isAutoFormat;
	}

	public String getIndentBy()
	{
		return indentBy;
	}

	public void setIndentBy(String indentBy)
	{
		this.indentBy = indentBy;
	}

	public int getReferredLineWidth()
	{
		return referredLineWidth;
	}

	public void setReferredLineWidth(int referredLineWidth)
	{
		this.referredLineWidth = referredLineWidth;
	}

	public void write(Document doc) throws IOException
	{
		w.write("<?xml version=\"");
		w.write(doc.getXmlVersion());
		w.write("\"");
		if (null != doc.getXmlEncoding())
		{
			w.write(" encoding=\"" + doc.getXmlEncoding() + "\"");
		}
		w.write("?>");
		if (null != doc.getDoctype())
			_write(doc.getDoctype());
		if (isAutoFormat)
		{
			NodeList nodes = doc.getChildNodes();
			for (int i = 0; i < nodes.getLength(); i++)
			{
				formattedWriteNode(nodes.item(i),0);
			}
		}
		else
		{
			NodeList nodes = doc.getChildNodes();
			for (int i = 0; i < nodes.getLength(); i++)
			{
				w.write(I.NEW_LINE);
				_writeNode(nodes.item(i));
			}
		}
	}

	public void write(Node node, int indent) throws IOException
	{
		if (isAutoFormat)
			this.formattedWriteNode(node,indent);
		else
			this._writeNode(node);

	}

	private void _write(DocumentType dt) throws IOException
	{
		w.write(I.NEW_LINE);
		w.write("<!DOCTYPE ");
		w.write(dt.getName());
		if (null == dt.getPublicId())
		{
			w.write(I.NEW_LINE);
			w.write("\tSYSTEM ");
			w.write("\"" + dt.getSystemId() + "\"");
		}
		else
		{
			w.write(I.NEW_LINE);
			w.write("\tPUBLIC ");
			w.write("\"" + dt.getPublicId() + "\"");
			w.write(I.NEW_LINE);
			w.write("\t\"" + dt.getSystemId() + "\"");
		}
		NamedNodeMap entities = dt.getEntities();
		if (entities.getLength() > 0)
		{
			w.write(" [ ");
			for (int i = 0; i < entities.getLength(); i++)
			{
				Node et = entities.item(i);
				w.write(I.NEW_LINE);
				w.write("\t<!ENTITY ");
				w.write(et.getNodeName());
				w.write(" \"");
				w.write(et.getNodeValue());
				w.write("\">");
			}
			w.write(I.NEW_LINE);
			w.write("]");
		}
		w.write(">");
	}

	private void _write(Element ele) throws IOException
	{
		w.write("<");
		w.write(ele.getNodeName());
		NamedNodeMap atts = ele.getAttributes();
		for (int i = 0; i < atts.getLength(); i++)
		{
			_write((Attr)atts.item(i));
		}
		NodeList nl = ele.getChildNodes();
		if (nl.getLength() > 0)
		{
			w.write(">");

			for (int i = 0; i < nl.getLength(); i++)
			{
				Node n = nl.item(i);
				_writeNode(n);
			}
			w.write("</");
			w.write(ele.getNodeName());
			w.write(">");
		}
		else
		{
			w.write("/>");
		}
	}

	private void _writeNode(Node node) throws IOException
	{
		if (XMLUtils.isElement(node))
		{
			_write((Element)node);
		}
		else if (XMLUtils.isComment(node))
		{
			_write((Comment)node);
		}
		else if (XMLUtils.isText(node))
		{
			_write((Text)node);
		}
		else if (XMLUtils.isCDATA(node))
		{
			_write((CDATASection)node);
		}
		else if (XMLUtils.isProcessing(node))
		{
			_write((ProcessingInstruction)node);
		}
		else
		{
			throw new IOException("Unsupportted node type " + node.getNodeType() + " -> \""
					+ node.toString() + "\"");
		}
	}

	private void _write(Attr a) throws IOException
	{
		w.write(" ");
		w.write(a.getName());
		w.write("=\"");
		w.write(XMLUtils.escapeXmlAttributeValue(a.getValue()));
		w.write("\"");
	}

	private void _write(Text text) throws IOException
	{
		w.write(XMLUtils.escapeXmlTextContent(text.getNodeValue()));
	}

	private void _write(Comment comment) throws IOException
	{
		w.write("<!--");
		w.write(comment.getNodeValue());
		w.write("-->");
	}

	private void _write(CDATASection cdata) throws IOException
	{
		w.write("<![CDATA[");
		w.write(cdata.getNodeValue());
		w.write("]]>");
	}

	private void _write(ProcessingInstruction processing) throws IOException
	{
		w.write("<?");
		w.write(processing.getTarget());
		w.write(" ");
		w.write(processing.getNodeValue());
		w.write("?>");
	}

	private void formattedWrite(Element ele, int depth) throws IOException
	{
		if (null == ele)
			return;
		String prefix = StringUtils.dup(indentBy,depth).toString();
		w.write(I.NEW_LINE);
		w.write(prefix);
		w.write("<");
		w.write(ele.getNodeName());
		NamedNodeMap atts = ele.getAttributes();
		for (int i = 0; i < atts.getLength(); i++)
		{
			formattedWrite((Attr)atts.item(i),depth);
		}
		NodeList nl = ele.getChildNodes();
		if (nl.getLength() > 0)
		{
			w.write(">");
			int cd = depth + 1;
			for (int i = 0; i < nl.getLength(); i++)
			{
				Node n = nl.item(i);
				formattedWriteNode(n,cd);
			}
			if (!(nl.getLength() == 1 && XMLUtils.isText(nl.item(0))))
			{
				w.write(I.NEW_LINE);
				w.write(prefix);
			}
			w.write("</");
			w.write(ele.getNodeName());
			w.write(">");
		}
		else
		{
			w.write("/>");
		}

	}

	private void formattedWriteNode(Node node, int cd) throws IOException
	{
		if (XMLUtils.isElement(node))
		{
			formattedWrite((Element)node,cd);
		}
		else if (XMLUtils.isComment(node))
		{
			formattedWrite((Comment)node,cd);
		}
		else if (XMLUtils.isText(node))
		{
			formattedWrite((Text)node,cd);
		}
		else if (XMLUtils.isCDATA(node))
		{
			formattedWrite((CDATASection)node,cd);
		}
		else if (XMLUtils.isProcessing(node))
		{
			formattedWrite((ProcessingInstruction)node,cd);
		}
		else
		{
			throw new IOException("Unsupportted node type " + node.getNodeType() + " -> \""
					+ node.toString() + "\"");
		}
	}

	private void formattedWrite(Attr a, int depth) throws IOException
	{
		w.write(" ");
		w.write(a.getName());
		w.write("=\"");
		w.write(XMLUtils.escapeXmlAttributeValue(a.getValue()));
		w.write("\"");
	}

	private void formattedWrite(Text text, int depth) throws IOException
	{
		if (null == text)
			return;
		String content = StringUtils.trim(text.getNodeValue());
		if (content.length() == 0)
			return;
		if (content.length() < 20 && text.getParentNode().getChildNodes().getLength() == 1)
		{
			this._write(text);
			return;
		}
		String prefix = StringUtils.dup(indentBy,depth).toString();
		String[] lines = content.split("[\n]");
		for (int i = 0; i < lines.length; i++)
		{
			w.write(I.NEW_LINE);
			w.write(prefix);
			w.write(XMLUtils.escapeXmlTextContent(lines[i]));
		}
	}

	private void formattedWrite(Comment comment, int depth) throws IOException
	{
		String prefix = StringUtils.dup(indentBy,depth).toString();
		w.write(I.NEW_LINE);
		w.write(prefix);
		w.write("<!--");
		w.write(I.NEW_LINE);
		String content = StringUtils.trim(comment.getNodeValue());
		String[] lines = content.split("[\n]");
		if (content.length() == 0)
			return;
		for (int i = 0; i < lines.length; i++)
		{
			w.write(prefix);
			w.write(lines[i]);
			w.write(I.NEW_LINE);
		}
		w.write(prefix);
		w.write("-->");
	}

	private void formattedWrite(CDATASection cdata, int depth) throws IOException
	{
		String prefix = StringUtils.dup(indentBy,depth).toString();
		w.write(I.NEW_LINE);
		w.write(prefix);
		_write(cdata);
	}

	private void formattedWrite(ProcessingInstruction processing, int depth) throws IOException
	{
		w.write(I.NEW_LINE);
		w.write("<?");
		w.write(processing.getTarget());
		w.write(I.NEW_LINE);
		w.write(StringUtils.trim(processing.getNodeValue()));
		w.write(I.NEW_LINE);
		w.write("?>");
	}

	@Override
	public void close() throws IOException
	{
		w.close();
	}

	@Override
	public void flush() throws IOException
	{
		w.flush();
	}

	@Override
	public void write(char[] cbuf, int off, int len) throws IOException
	{
		w.write(cbuf,off,len);
	}

}
